package com.easipass.framework.eirframeworkcoredsl.graph;

import com.easipass.framework.eirframeworkcoredsl.TransitionNode;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import java.util.*;
import java.util.Map.Entry;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Graph {

    public List<Node> nodes;

    public List<Link> links;

    Graph() {
        this.nodes = new ArrayList<>();
        this.links = new ArrayList<>();
    }

    public Graph(List<Node> nodes, List<Link> links) {
        this.nodes = nodes;
        this.links = links;
    }

    public List<Node> getNodes() {
        return this.nodes;
    }

    public List<Link> getLinks() {
        return this.links;
    }

    @Override
    public String toString() {
        return "Graph:  nodes=#" + nodes.size() + "  links=#" + links.size() + "\n" + nodes + "\n" + links;
    }

    public String toVerboseString() {
        StringBuilder s = new StringBuilder();
        for (Node n : nodes) {
            s.append("[").append(n.id).append(":");
            if (n.getLabel() != null) {
                s.append(n.getLabel()).append(":");
            }
            s.append(n.name);
            if (n.properties != null) {
                for (Entry<String, String> property : n.properties.entrySet()) {
                    s.append(":").append(property.getKey()).append("=").append(property.getValue());
                }
            }
            s.append("]");
        }
        for (Link l : links) {
            s.append("[" + (l.getTransitionName() == null ? "" : l.getTransitionName() + ":") + l.from + "-" + l.to
                    + "]");
        }
        return s.toString();
    }

    public String toVerboseJson() {
        StringBuilder s = new StringBuilder();
        s.append("{\"nodeList\":[");
        for (Node n : nodes) {
            s.append("{")
                    .append("\"type\":\"")
                    .append(n.type)
                    .append("\",")
                    .append("\"nodeName\": \"")
                    .append(n.name)
                    .append("\",")
                    .append("\"id\": \"")
                    .append(n.primaryKey)
                    .append("\"},");
        }
        if (nodes.size() > 0){
            s.deleteCharAt(s.length()-1);
        }
        s.append("]");
        s.append(",");
        s.append("\"linkList\": [");
        for (Link l : links) {
            s.append("{\"type\": \"link\",")
                    .append("\"id\": \"")
                    .append(l.id)
                    .append("\",")
                    .append("\"sourceId\": \"")
                    .append(findNodeById(l.from).primaryKey)
                    .append("\",")
                    .append("\"targetId\": \"")
                    .append(findNodeById(l.to).primaryKey)
                    .append("\"},");

        }
        if (links.size() > 0){
            s.deleteCharAt(s.length()-1);
        }
        s.append("]}");
        return s.toString();
    }

    public String toDSLText() {
        StringBuilder graphText = new StringBuilder();
        List<Node> unvisitedNodes = new ArrayList<>();
        List<Link> unfollowedLinks = new ArrayList<>();
        unvisitedNodes.addAll(nodes);
        unfollowedLinks.addAll(links);

        Node start = findStartNode();
        unvisitedNodes.remove(start);
        Node end = findEndNode();
        unvisitedNodes.remove(end);
        Node fail = findNodeByName("FAIL");
        if (fail != null) {
            unvisitedNodes.remove(fail);
        }
        if (start == null || end == null) {
            throw new IllegalStateException("Graph is malformed - problems finding START and END nodes");
        }
        List<Link> toFollow = findLinksFrom(start, false);
        followLinks(graphText, toFollow, null, unvisitedNodes, unfollowedLinks, false);

        if (unvisitedNodes.size() != 0) {
            int loopCount = 0;
            while (unvisitedNodes.size() != 0 && loopCount < 10000) {
                Node nextHead = findAHead(unvisitedNodes, unfollowedLinks);
                unvisitedNodes.remove(nextHead);
                toFollow = findLinksFrom(nextHead, false);
                if (toFollow.size() != 0) {
                    graphText.append(" && ");
                    printNode(graphText, nextHead, unvisitedNodes);
                    followLinks(graphText, toFollow, null, unvisitedNodes, unfollowedLinks, false);
                }
                loopCount++;
            }
        }

        return graphText.toString();
    }

    private Node findAHead(List<Node> unvisitedNodes, List<Link> unvisitedLinks) {
        if (unvisitedNodes.size() == 0) {
            return null;
        }
        Node candidate = unvisitedNodes.get(0);
        boolean changedCandidate = true;
        while (changedCandidate) {
            changedCandidate = false;
            for (Link link : unvisitedLinks) {
                if (link.to == candidate.id) {
                    changedCandidate = true;
                    candidate = findNodeById(link.from);
                }
            }
        }
        return candidate;
    }

    private void followLinks(StringBuilder graphText, List<Link> toFollow, Node nodeToTerminateFollow,
                             List<Node> unvisitedNodes, List<Link> unfollowedLinks, boolean inNestedSplit) {
        while (toFollow.size() != 0) {
            if (toFollow.size() > 1) { // SPLIT
                if (!inNestedSplit && graphText.length() != 0) {
                    graphText.append(" && ");
                }
                graphText.append("<");
                Node endOfSplit = findEndOfSplit(toFollow);
                if (toFollow.size() > 2) {
                    Map<Node, List<Link>> nestedSplits = findNestedSplits(toFollow, endOfSplit);
                    int i = 0;
                    for (Entry<Node, List<Link>> nestedSplit : nestedSplits.entrySet()) {
                        Node endOfNestedSplit = nestedSplit.getKey();
                        List<Link> nestedSplitLinks = nestedSplit.getValue();
                        followLinks(graphText, nestedSplitLinks, endOfNestedSplit, unvisitedNodes, unfollowedLinks,
                                true);
                        toFollow.removeAll(nestedSplitLinks);
                        graphText.append(" && ");
                        followNode(graphText, endOfNestedSplit, endOfSplit, unvisitedNodes, unfollowedLinks);
                        i++;
                        if (i < nestedSplits.size()) {
                            graphText.append(" || ");
                        }
                    }
                    if (!toFollow.isEmpty() && !nestedSplits.isEmpty()) {
                        graphText.append(" || ");
                    }
                }
                for (int i = 0; i < toFollow.size(); i++) {
                    if (i > 0) {
                        graphText.append(" || ");
                    }
                    Link l = toFollow.get(i);
                    followLink(graphText, l, endOfSplit, unvisitedNodes, unfollowedLinks);
                }
                graphText.append(">");
                if (endOfSplit == null || endOfSplit.isEnd()) {
                    break;
                }
                if (endOfSplit == nodeToTerminateFollow) {
                    break;
                }
                unvisitedNodes.remove(endOfSplit);
                if (!endOfSplit.isSync()) {
                    graphText.append(" && ");
                    printNode(graphText, endOfSplit, unvisitedNodes);
                    List<Link> transitionalLinks = findLinksFrom(endOfSplit, false);
                    printTransitions(graphText, unvisitedNodes, unfollowedLinks, transitionalLinks, null);
                }
                toFollow = findLinksFromWithoutTransitions(endOfSplit, false);
            } else if (toFollow.size() == 1) { // FLOW
                Link linkToFollow = toFollow.get(0);
                Node linkToFollowTarget = findNodeById(linkToFollow.to);
                if (linkToFollowTarget != nodeToTerminateFollow) {
                    Node linkFromNode = findNodeById(linkToFollow.from);
                    if (Node.START.equals(linkFromNode.type)) {
                        graphText.append(":")
                                .append(linkFromNode.name)
                                .append(" > ");
                    }else if(Node.END.equals(linkToFollowTarget.type)){
                        graphText.append(" > :");
                    }else {
                        graphText.append(" && ");
                    }

                    followLink(graphText, linkToFollow, nodeToTerminateFollow, unvisitedNodes, unfollowedLinks);
                }
                break;
            }
        }
    }

    private List<Link> findSubsetOfLinksThatReachNode(List<Link> links, Link linkToIgnore, Node node) {
        List<Link> result = null;
        for (Link link : links) {
            if (link == linkToIgnore) {
                continue;
            }
            if (foundInChain(link, node)) {
                if (result == null) {
                    result = new ArrayList<>();
                }
                result.add(link);
            }
        }
        if (result != null) {
            result.add(linkToIgnore);
        }
        return result;
    }

    private Map<Node, List<Link>> findNestedSplits(List<Link> toFollow, Node end) {
        Map<Node, List<Link>> nestedSplits = new LinkedHashMap<>();
        for (Link link : toFollow) {
            Node successor = findNodeById(link.to);
            while (successor != null && successor != end) {
                List<Link> commonLinks = findSubsetOfLinksThatReachNode(toFollow, link, successor);
                if (commonLinks != null) {
                    boolean insertThisOne = true;
                    Node forRemoval = null;
                    for (Entry<Node, List<Link>> subsplit : nestedSplits.entrySet()) {
                        if (equalLinkLists(subsplit.getValue(), commonLinks)) {
                            if (isSuccessor(subsplit.getKey(), successor)) {
                                insertThisOne = false;
                            } else {
                                forRemoval = subsplit.getKey();
                            }
                        }
                    }
                    if (insertThisOne) {
                        if (forRemoval != null) {
                            nestedSplits.remove(forRemoval);
                        }
                        nestedSplits.put(successor, commonLinks);
                    }
                }
                List<Link> links = findLinksFrom(successor, true);
                if (links.size() == 0) {
                    successor = null;
                } else if (links.size() == 1) {
                    successor = findNodeById(links.get(0).to);
                } else {
                    if (countLinksWithoutTransitions(links) == 0 || countLinksWithoutTransitions(links) == 1) {
                        successor = findNodeById(links.get(0).to);
                    } else {
                        while (countLinksWithoutTransitions(links) > 1) {
                            successor = findEndOfSplit(links);
                            links = findLinksFrom(successor, true);
                        }
                    }
                }
            }
        }
        List<Entry<Node, List<Link>>> toSort = new ArrayList<>(nestedSplits.entrySet());
        Collections.sort(toSort, new NestedSplitComparator());
        nestedSplits = new LinkedHashMap<>();
        for (Entry<Node, List<Link>> entry : toSort) {
            nestedSplits.put(entry.getKey(), entry.getValue());
        }
        return nestedSplits;
    }

    class NestedSplitComparator implements Comparator<Entry<Node, List<Link>>> {
        @Override
        public int compare(Entry<Node, List<Link>> splitA, Entry<Node, List<Link>> splitB) {
            Node endOfA = splitA.getKey();
            Node endOfB = splitB.getKey();
            if (endOfA == endOfB) {
                return 0;
            }
            if (isSuccessor(endOfA, endOfB)) {
                return -1;
            }
            return 1;
        }
    }

    private boolean equalLinkLists(List<Link> list1, List<Link> list2) {
        return list1.containsAll(list2) && list2.containsAll(list1);
    }

    private boolean isSuccessor(Node a, Node b) {
        for (Link link : findLinksFrom(a, true)) {
            if (foundInChain(link, b)) {
                return true;
            }
        }
        return false;
    }

    private Node findEndOfSplit(List<Link> toFollow) {
        if (toFollow.size() == 0) {
            return null;
        }
        if (toFollow.size() == 1) {
            return findNodeById(toFollow.get(0).to);
        }
        Link link = toFollow.get(0);
        Node nextCandidate = findNodeById(link.to);
        while (nextCandidate != null) {
            boolean allLinksLeadToTheCandidate = true;
            for (int l = 1; l < toFollow.size(); l++) {
                if (!foundInChain(toFollow.get(l), nextCandidate)) {
                    allLinksLeadToTheCandidate = false;
                    break;
                }
            }
            if (allLinksLeadToTheCandidate) {
                return nextCandidate;
            }
            List<Link> links = findLinksFrom(nextCandidate, true);
            if (links.size() == 0) {
                nextCandidate = null;
            } else if (links.size() == 1) {
                nextCandidate = findNodeById(links.get(0).to);
            } else {
                if (countLinksWithoutTransitions(links) == 0 || countLinksWithoutTransitions(links) == 1) {
                    nextCandidate = findNodeById(links.get(0).to);
                } else {
                    while (countLinksWithoutTransitions(links) > 1) {
                        nextCandidate = findEndOfSplit(links);
                        links = findLinksFrom(nextCandidate, true);
                    }
                }
            }
        }
        throw new IllegalStateException("Unable to find end of split");
    }

    private boolean foundInChain(Link link, Node candidate) {
        String targetId = link.to;
        Node targetNode = findNodeById(targetId);
        if (targetNode == candidate) {
            return true;
        }
        List<Link> outboundLinks = findLinksFrom(targetNode, true);
        for (Link lnk : outboundLinks) {
            if (foundInChain(lnk, candidate)) {
                return true;
            }
        }
        return false;
    }

    private int countLinksWithoutTransitions(List<Link> links) {
        int count = 0;
        for (Link link : links) {
            if (!link.hasTransitionSet()) {
                count++;
            }
        }
        return count;
    }

    private void printNode(StringBuilder graphText, Node node, List<Node> unvisitedNodes) {
        unvisitedNodes.remove(node);
        String nameInDSL = node.name;
        if (node.getLabel() != null) {
            graphText.append(node.getLabel()).append(": ");
        }
        graphText.append(nameInDSL);
        if (node.properties != null) {
            for (Entry<String, String> entry : node.properties.entrySet()) {
                graphText.append(" ");
                String propertyValue = entry.getValue();
                if (propertyValue.contains(" ") && !propertyValue.startsWith("'")) {
                    propertyValue = "'" + propertyValue + "'";
                }
                graphText.append("--").append(entry.getKey()).append("=").append(propertyValue);
            }
        }
    }

    private void followNode(StringBuilder graphText, Node node, Node nodeToFinishFollowingAt, List<Node> unvisitedNodes,
                            List<Link> unfollowedLinks) {
        List<Link> toFollow = findLinksFrom(node, false);
        boolean singleSplitNecessary = false;
        Node commonTarget = null;
        if (toFollow.size() > 1 && allTransitionsButOne(toFollow)) {
            try {
                commonTarget = findEndOfSplit(sortNotTransitionLinkFirst(toFollow));
                singleSplitNecessary =
                        commonTarget != null && !commonTarget.type.equals(Node.END) &&
                                (nodeToFinishFollowingAt == null || !nodeToFinishFollowingAt.equals(commonTarget));
            } catch (IllegalStateException ise) {
            }
        }

        if (singleSplitNecessary) {
            graphText.append("<");
            printNode(graphText, node, unvisitedNodes);
            printTransitions(graphText, unvisitedNodes, unfollowedLinks, toFollow, commonTarget);
            graphText.append(">");
        } else {
            printNode(graphText, node, unvisitedNodes);
            printTransitions(graphText, unvisitedNodes, unfollowedLinks, toFollow, nodeToFinishFollowingAt);
        }
        followLinks(graphText, toFollow, nodeToFinishFollowingAt, unvisitedNodes, unfollowedLinks, false);
    }

    List<Link> sortNotTransitionLinkFirst(List<Link> links) {
        List<Link> result = new ArrayList<>();
        for (Link l : links) {
            if (l.hasTransitionSet()) {
                result.add(0, l);
            } else {
                result.add(l);
            }
        }
        return result;
    }

    private boolean allTransitionsButOne(List<Link> links) {
        int transitionCount = 0;
        for (Link l : links) {
            if (l.hasTransitionSet()) {
                transitionCount++;
            }
        }
        return (links.size() - transitionCount) == 1;
    }

    private void followLink(StringBuilder graphText, Link link, Node nodeToFinishFollowingAt, List<Node> unvisitedNodes,
                            List<Link> unfollowedLinks) {
        unfollowedLinks.remove(link);
        followNode(graphText, findNodeById(link.to), nodeToFinishFollowingAt, unvisitedNodes, unfollowedLinks);
    }

    private void printTransitions(StringBuilder graphText, List<Node> unvisitedNodes, List<Link> unfollowedLinks,
                                  List<Link> toFollow, Node nodeToFinishFollowingAt) {
        for (Iterator<Link> iterator = toFollow.iterator(); iterator.hasNext(); ) {
            Link l = iterator.next();
            if (l.hasTransitionSet()) {
                String transitionName = l.getTransitionName();
                boolean isStatusText = true;
                try {
                    Integer.parseInt(transitionName);
                    isStatusText = false;
                } catch (NumberFormatException nfe) {
                    // it is text
                }
                if (isStatusText && !transitionName.startsWith("'")) {
                    transitionName = "'" + transitionName + "'";
                }
                Node transitionTarget = findNodeById(l.to);
                String transitionTargetName = transitionTarget.name;
                if (transitionTargetName.equals("FAIL")) {
                    transitionTargetName = TransitionNode.FAIL;
                } else if (transitionTargetName.equals("END")) {
                    transitionTargetName = TransitionNode.END;
                } else if (transitionTarget.getLabel() != null) {
                    transitionTargetName = transitionTarget.getLabel() + ": " + transitionTargetName;
                }
                graphText.append(" ").append(transitionName).append("->").append(transitionTargetName);
                unfollowedLinks.remove(l);
                List<Link> linksFromTheTransitionTarget = findLinksFrom(transitionTarget, false);
                if (linksFromTheTransitionTarget.isEmpty()
                        || allLinksTarget(linksFromTheTransitionTarget, nodeToFinishFollowingAt)) {
                    unvisitedNodes.remove(transitionTarget);
                }
                iterator.remove();
            }
        }
    }

    private boolean allLinksTarget(List<Link> linksFromTheTransitionTarget, Node nodeToFinishFollowingAt) {
        if (nodeToFinishFollowingAt == null) {
            return false;
        }
        for (Link link : linksFromTheTransitionTarget) {
            if (!link.to.equals(nodeToFinishFollowingAt.id)) {
                return false;
            }
        }
        return true;
    }

    private Node findNodeById(String id) {
        for (Node n : nodes) {
            if (n.id.equals(id)) {
                return n;
            }
        }
        return null;
    }

    private Node findNodeByName(String name) {
        for (Node n : nodes) {
            if (n.name.equals(name)) {
                return n;
            }
        }
        return null;
    }

    private Node findStartNode() {
        for (Node n : nodes) {
            if (n.type.equals(Node.START)) {
                return n;
            }
        }
        return null;
    }

    private Node findEndNode() {
        for (Node n : nodes) {
            if (n.type.equals(Node.END)) {
                return n;
            }
        }
        return null;
    }

    private List<Link> findLinksFromWithoutTransitions(Node n, boolean includeThoseLeadingToEnd) {
        List<Link> result = new ArrayList<>();
        for (Link link : links) {
            if (link.from.equals(n.id)) {
                if ((!link.hasTransitionSet()
                        && (includeThoseLeadingToEnd || !findNodeById(link.to).name.equals("END")))
                        || (link.hasTransitionSet() && link.getTransitionName().equals("'*'"))) {
                    result.add(link);
                }
            }
        }
        return result;
    }

    private boolean hasNoProperties(Link link) {
        return link.properties == null || link.properties.size() == 0;
    }

    private List<Link> findLinksFrom(Node n, boolean includeThoseLeadingToEnd) {
        List<Link> result = new ArrayList<>();
        for (Link link : links) {
            if (link.from.equals(n.id)) {
                if (includeThoseLeadingToEnd
                        || !(findNodeById(link.to).name.equals("END") && hasNoProperties(link))) {
                    result.add(link);
                }
            }
        }
        return result;
    }
}
